Skip to content

fix(parser): bind source-counter-gated rider pronouns to the source (Gemstone Mine #6507) - #6559

Merged
matthewevans merged 10 commits into
phase-rs:mainfrom
jeffrey701:fix/6507-depletion-sacrifice-rider
Aug 1, 2026
Merged

fix(parser): bind source-counter-gated rider pronouns to the source (Gemstone Mine #6507)#6559
matthewevans merged 10 commits into
phase-rs:mainfrom
jeffrey701:fix/6507-depletion-sacrifice-rider

Conversation

@jeffrey701

@jeffrey701 jeffrey701 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes Gemstone Mine (#6507) and the whole source-counter-conditioned rider class: the depletion-land sacrifice rider — "{T}, Remove a mining counter from this land: Add one mana of any color. If there are no mining counters on this land, sacrifice it." — never sacrificed the land after its last counter was removed.

Root cause

Parse-time anaphor mis-binding, not a runtime gap. The rider's sub-ability parsed to Sacrifice { target: ParentTarget }. A mana ability has no targets (CR 605.1a), so ParentTarget resolves to an empty set at resolution (game/effects/sacrifice.rseffect_object_targets(ParentTarget, []) is empty → the sacrifice silently no-ops and the land survives).

The effect-chain parser already binds a bare "it" to SelfRef when the gating condition references the source object, via condition_refs_source_object. That predicate recognized source-tapped / source-entered / source-attached conditions, but not a source-scoped counter threshold (QuantityCheck over CountersOn { scope: Source }) — the exact shape these riders produce. So the counter-gated body pronoun fell through to ParentTarget (and, on typed triggers, to TriggeringSource).

Fix

One additive predicate extension in crates/engine/src/parser/oracle_effect/mod.rs (+41/-0):

  • a new exhaustive QuantityExpr walker quantity_expr_reads_source_counters (no wildcard arm — a future variant must be classified; mirrors quantity_expr_uses_recipient);
  • a QuantityCheck { lhs, rhs, .. } arm in condition_refs_source_object returning true when either side reads counters on the source.

This single change drives both existing consumers of the predicate: the chunk-subject threading (mod.rs:28564) now supplies SelfRef, and the ParentTarget rewrite guard (mod.rs:29335) now skips these chunks. No runtime files change; the AST is now what the card says (SelfRef), and sacrifice.rs's existing SelfRef pool resolution + CR 400.7 epoch guard do the rest.

Corrects the binding for ~21 cards: the 5 Mercadian depletion lands, Gemstone Mine, Tourach's Gate, Contested Game Ball, Daredevil Dragster, Dawn of a New Age, Evolved Spinoderm (Sacrifice ParentTarget → SelfRef); Blood Spatter Analysis, Charitable Levy, Decree of Silence, Last Light of Durin's Day, The Heron Moon, ED-E (Sacrifice/PutCounter TriggeringSource → SelfRef); Heirloom Mirror, Ludevic's Test Subject, Replicating Ring, Smoldering Egg (RemoveCounter ParentTarget → SelfRef). Grasping Shadows / Soulcipher Board flip Transform SelfRef → ParentTarget, which is behavior-neutral (the transform effect handler falls back to the source on empty targets).

Files changed

  • crates/engine/src/parser/oracle_effect/mod.rs — the fix (helper + match arm)
  • crates/engine/src/parser/oracle_effect/tests.rs — predicate unit test (Source→true incl. wrapped/Not/And; Target/Recipient scopes→false)
  • crates/engine/src/parser/oracle_tests.rs — 2 parser SHAPE tests (Gemstone Mine, Last Light) with reach-guards
  • crates/engine/tests/integration/gemstone_mine_depletion_sacrifice_6507.rs — 5 runtime tests
  • crates/engine/tests/integration/main.rs — mod line

CR references

  • CR 122.1 (counters) + CR 608.2k (an effect referring to an untargeted object previously referred to by the ability still affects it) — the new predicate arm / helper.
  • CR 605.1a (mana ability requires no target) + CR 605.3b (mana ability resolves immediately) + CR 701.21a (sacrifice) — test annotations.

Implementation method (required)

Method: /engine-implementer

Track

Developer

LLM

Model: claude-opus-4-8[1m]
Thinking: high

Verification

  • Required checks ran clean.

  • Gate A output below is for the current committed head.

  • Final review-impl below is clean for the current committed head.

  • Both anchors cite existing analogous code at the same seam.

  • cargo test -p engine --lib17593 passed, 0 failed, 6 ignored (baseline 17590 + 3 new)

  • cargo test -p engine --test integration3874 passed, 0 failed, 2 ignored (baseline 3869 + 5 new)

  • cargo fmt --all -- --check — clean

  • ./scripts/gen-card-data.sh — regenerated; parse-diff audit of the source-counter rider shape class: 23 cards changed, all either correctness heals or behavior-neutral (Transform empty-target→source fallback), zero regressions.

  • RED/GREEN: tests 1/3/4/5 + both shape tests fail on the pre-fix ParentTarget/TriggeringSource binding and pass after; test 2 is the paired over-trigger negative with positive reach-guards.

Gate A

Gate A PASS head=1c74fedb5bef8d76fb45de2c5e22833208179d1d base=6ae8737cdab0fa1ed291cad0f0808473a90f4cf8

Anchored on

  • crates/engine/src/parser/oracle_trigger.rs:1297 — trigger-body parse constructs effect_ctx with subject: Some(trigger_subject.clone()) (the established subject-threading seam that already binds trigger-borne riders correctly).
  • crates/engine/src/parser/oracle_effect/mod.rs:2876 — delayed-trigger body parse threads inner_ctx.subject = Some(TargetFilter::SelfRef) for the self-referential case — the exact SelfRef subject-threading this change completes for the counter-gated chunk path (mod.rs:28564).

Final review-impl

Final review-impl PASS head=1c74fedb5bef8d76fb45de2c5e22833208179d1d

Claimed parse impact

  • Gemstone Mine, Peat Bog, Hickory Woodlot, Remote Farm, Sandstone Needle, Saprazzan Skerry, Tourach's Gate, Contested Game Ball, Daredevil Dragster, Dawn of a New Age, Evolved Spinoderm, Blood Spatter Analysis, Charitable Levy, Decree of Silence, Last Light of Durin's Day, The Heron Moon, ED-E Lonesome Eyebot, Heirloom Mirror, Ludevic's Test Subject, Replicating Ring, Smoldering Egg (correctness heals); Grasping Shadows, Soulcipher Board (behavior-neutral Transform relabel).

Validation Failures

None.

CI Failures

None.


Tier: Frontier

Summary by CodeRabbit

  • Bug Fixes

    • Improved parsing of source-specific counter conditions, including nested, negated, combined, and arithmetic expressions.
    • Corrected pronoun and target binding in effects involving counter checks and sacrifice abilities.
    • Improved handling of recipient counter conditions to preserve the correct affected object.
    • Fixed land depletion sacrifice effects and related abilities so they consistently affect the intended objects across activation and payment scenarios.
  • Tests

    • Added regression and integration coverage for counter-based abilities, land depletion sacrifices, payment paths, and granted abilities.

@jeffrey701
jeffrey701 requested a review from matthewevans as a code owner July 23, 2026 18:38
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The parser now detects source-scoped counters through nested quantity expressions, rewrites leading bare-recipient counter conditions after typed targets, and adjusts pronoun and parent-target binding. Parser and integration tests cover related counter and sacrifice scenarios.

Changes

Oracle counter binding

Layer / File(s) Summary
Counter condition detection
crates/engine/src/parser/oracle_effect/mod.rs, crates/engine/src/parser/oracle_effect/tests.rs, crates/engine/src/parser/oracle_nom/condition.rs
Recursive quantity traversal detects source-counter references. A helper identifies leading bare-recipient counter conditions. Tests cover nested, accepted, and rejected forms.
Clause binding and condition rewriting
crates/engine/src/parser/oracle_effect/mod.rs, crates/engine/src/parser/oracle_static/...
Clause parsing tracks prior typed referents, selects source or target bindings, resolves parent targets, and rewrites applicable HasCounters conditions to RecipientHasCounters.
Parser and integration regression coverage
crates/engine/src/parser/oracle_tests.rs, crates/engine/tests/integration/...
Tests verify source sacrifice riders, recipient binding after typed targets, depletion-land sacrifice paths, and countered-target abilities.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested reviewers: minion1227, lgray, matthewevans

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.60% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main parser fix for source-counter-gated rider pronoun binding.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the needs-maintainer AI-contribution PR requires human triage (Non-dev track or unresolved gaps) label Jul 23, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
crates/engine/src/parser/oracle_effect/tests.rs (1)

28634-28709: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Wrapper coverage is partial: only Ref/Offset are exercised.

quantity_expr_reads_source_counters has 8 recursive wrapper arms (DivideRounded, Offset, ClampMin, Multiply, Sum, Max, UpTo, Power, Difference), but this test only drives propagation through Offset. Consider adding a couple more positive cases (e.g. Sum/Difference, which take two sub-expressions and are the likeliest place for a copy-paste slip) to lock in the exhaustive-walk guarantee the doc comment advertises.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/parser/oracle_effect/tests.rs` around lines 28634 - 28709,
Add positive test cases in
condition_refs_source_object_source_counter_quantity_check covering additional
quantity_expr_reads_source_counters wrappers, especially binary Sum and
Difference expressions containing a source-scoped CountersOn reference. Keep the
assertions focused on propagation through both operands and preserve the
existing non-source and wrapper coverage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/engine/src/parser/oracle_effect/tests.rs`:
- Around line 28634-28709: Add positive test cases in
condition_refs_source_object_source_counter_quantity_check covering additional
quantity_expr_reads_source_counters wrappers, especially binary Sum and
Difference expressions containing a source-scoped CountersOn reference. Keep the
assertions focused on propagation through both operands and preserve the
existing non-source and wrapper coverage.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4862da73-1499-4d5a-b7f0-3eb506842b5d

📥 Commits

Reviewing files that changed from the base of the PR and between e731b73 and 1c74fed.

📒 Files selected for processing (5)
  • crates/engine/src/parser/oracle_effect/mod.rs
  • crates/engine/src/parser/oracle_effect/tests.rs
  • crates/engine/src/parser/oracle_tests.rs
  • crates/engine/tests/integration/gemstone_mine_depletion_sacrifice_6507.rs
  • crates/engine/tests/integration/main.rs

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown

Parse changes introduced by this PR · 18 card(s), 6 signature(s) (baseline: main b45555a71b48)

🟡 Modified fields (6 signatures)

  • 5 cards · 🔄 ability/Sacrifice · changed field target: parent targetself
    • Affected (first 3): Contested Game Ball, Daredevil Dragster, Dawn of a New Age (+2 more)
  • 5 cards · 🔄 ability/Sacrifice · changed field target: triggering sourceself
    • Affected (first 3): Blood Spatter Analysis, Charitable Levy, Decree of Silence (+2 more)
  • 4 cards · 🔄 ability/RemoveCounter · changed field target: parent targetself
    • Affected (first 3): Heirloom Mirror, Ludevic's Test Subject, Replicating Ring (+1 more)
  • 2 cards · 🔄 ability/Transform · changed field target: selfparent target
    • Affected (first 3): Grasping Shadows, Soulcipher Board
  • 1 card · 🔄 ability/Draw · changed field conditional: P1P1 counters on self ≥ 1P1P1 counters on recipient ≥ 1
    • Affected (first 3): Dual-Sun Technique
  • 1 card · 🔄 ability/PutCounter · changed field target: triggering sourceself
    • Affected (first 3): ED-E, Lonesome Eyebot

1 card(s) had Oracle-text changes (errata/reprint) — excluded as non-parser.

@matthewevans

Copy link
Copy Markdown
Member

Request changes — the diagnosis, the seam, and the CR work are all correct, but the predicate is one degree too broad and lands a functional regression on Revelation of Power, a card outside the claimed scope.

🔴 Blocker

Revelation of Power loses its conditional grant entirely

crates/engine/src/parser/oracle_effect/mod.rs — the new AbilityCondition::QuantityCheck arm in condition_refs_source_object.

Oracle text, verbatim from Scryfall (Instant, Streets of New Capenna):

Target creature gets +2/+2 until end of turn. If it has a counter on it, it also gains flying and lifelink until end of turn.

Both pronouns anaphor to target creature. The card is an Instant, so it can carry no counters and can gain no flying/lifelink.

The parse-diff sticky for this head reports it twice, and it is absent from ## Claimed parse impact:

- **1 card** · 🔄 ability/grant Flying, grant Lifelink · changed field `affects`: `parent target` → `self`
  - Affected (first 3): Revelation of Power
- **1 card** · 🔄 ability/grant Flying, grant Lifelink · changed field `target`: `parent target` → `∅`
  - Affected (first 3): Revelation of Power

Baseline AST on main (from data/card-data.json) is already correct, and shows exactly why the new arm fires:

"static_abilities": [{
  "affected": { "type": "ParentTarget" },            //  correct: the target creature
  "modifications": [ {"type":"AddKeyword","keyword":"Flying"},
                     {"type":"AddKeyword","keyword":"Lifelink"} ],
  "condition": { "type": "QuantityComparison",
    "lhs": { "type":"Ref", "qty": { "type":"CountersOn",
             "scope": { "type":"Source" } } },       //  pre-existing latent mis-scope
    "comparator": "GE", ... }
}]

The condition's scope: Source here is not a real source reference — it is an unresolved bare "it" that should have lowered to the parent target. That mis-scope was harmless while affected stayed ParentTarget. The new arm keys off precisely this shape, drives chunk_subject → SelfRef (mod.rs:28564), and rebinds the grant to the Instant. The rider becomes a permanent no-op: the card loses its second sentence. This is the engine_regress bucket — a card losing a previously-working handler.

The cited rule also does not reach this case. CR 608.2k, verbatim:

608.2k If an ability's effect refers to a specific untargeted object that has been previously referred to by that ability's cost or trigger condition, it still affects that object even if the object has changed characteristics.

Gemstone Mine qualifies — its cost, "Remove a mining counter from this land", refers to the source. Revelation of Power has neither a cost nor a trigger condition referring to the source; the predicate is reading an intervening-if gate in the effect body, which is a strictly wider surface than 608.2k licenses.

The discriminator you need is already visible in the two cards. A true source gate names its object — "no mining counters on this land", "If Blaster has no +1/+1 counters on it". A false positive gates on a bare pronoun — "If it has a counter on it" — inside a chain whose prior clause already declared a chosen target. Narrow the arm so it fires only when the counter reference came from an explicit source noun phrase (~ / "this land" / the card name), or suppress it when the chunk's chain already carries a chosen-target referent (chain_prior_referent_is_chosen_target / parent_target_available are both in scope at mod.rs:28564). Either gate keeps all 21 intended heals and drops Revelation of Power out of the class.

🟡 Non-blocking

Claimed set does not match the measured set. The body states "23 cards changed, all either correctness heals or behavior-neutral … zero regressions", and the claimed list also has 23 names — but the two sets are not identical. Revelation of Power is measured and unclaimed, so one claimed name is not actually in the diff. The matching totals made the substitution invisible. Worth re-deriving the claim from the artifact rather than by count.

Possible incomplete class coverage. Scanning data/card-data.json for the predicate's shape (source-scoped CountersOn gate + ParentTarget/TriggeringSource binding) also surfaces Hostile Hostel and Blaster, Morale Booster, neither of which appears in the parse diff. Blaster's back face reads "Move X +1/+1 counters from Blaster onto another target artifact. … If Blaster has no +1/+1 counters on it, convert it." — an explicitly-named source gate on a targeted activated ability, where the current ParentTarget binding would convert the targeted artifact instead of Blaster. If a guard is holding these back, the class fix is partial. Confidence: moderate — my scan is a local approximation over a main snapshot, not the CI artifact, so please confirm against the full parse-diff artifact rather than the truncated sticky (the +8 more buckets may hide further unclaimed cards).

CodeRabbit's nitpick is fair. quantity_expr_reads_source_counters has nine recursive arms; the unit test drives propagation through Ref/Offset only. Adding Sum/Difference cases would lock in the exhaustive-walk guarantee the doc comment advertises — the two-operand arms are where a copy-paste slip would hide.

✅ Clean

  • Root-cause analysis is correct and precisely stated. ParentTarget does fall to the _ => arm of effect_object_targets (game/effects/mod.rs:251), resolving to the ability's chosen targets — empty for a mana ability per CR 605.1a. The Gemstone Mine diagnosis holds exactly as written.
  • Right seam, no new vocabulary. Extending the existing condition_refs_source_object predicate drives both of its existing consumers rather than adding a parallel path, and no new enum variant was introduced.
  • The Transform relabel is genuinely behavior-neutral, and I verified it rather than taking the claim. transform_effect::resolve dispatches on ability.targets.as_slice() with [] => ability.source_id (transform_effect.rs:23-30) and never consults the target filter, so Grasping Shadows / Soulcipher Board are unaffected. Confirming this in the body was the right call.
  • Every CR citation grep-verifies against docs/MagicCompRules.txt: 122.1 (counters), 608.2k, 605.1a, 605.3b, 701.21a. No invented numbers — this is consistently done well in your PRs.
  • The exhaustive QuantityExpr walker has no wildcard arm, so a future variant becomes a compile error instead of a silent false. Correct instinct, and the quantity_expr_uses_recipient mirror is the right precedent to follow.
  • Integration test is registeredmod gemstone_mine_depletion_sacrifice_6507; is present in crates/engine/tests/integration/main.rs, so the 5 runtime tests actually run.
  • Shape tests carry non-vacuity reach-guards (zero parse warnings, no Effect::Unimplemented anywhere in the parse, asserted ability count) before the shape assertions. This is exactly the guard that keeps a negative assertion from passing for the wrong reason.

Recommendation: narrow the QuantityCheck arm to explicit source noun phrases — or suppress it when the chain already has a chosen-target referent — and add a regression test pinning Revelation of Power's rider to ParentTarget. Then regenerate the parse diff and re-derive the claimed-impact list from the artifact, confirming whether Hostile Hostel and Blaster, Morale Booster should be in or out of the class. The Gemstone Mine fix itself is sound and worth landing once the blast radius is contained.

@matthewevans matthewevans removed the needs-maintainer AI-contribution PR requires human triage (Non-dev track or unresolved gaps) label Jul 25, 2026
@github-actions github-actions Bot added the needs-maintainer AI-contribution PR requires human triage (Non-dev track or unresolved gaps) label Jul 25, 2026

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current head 11b490a remains blocked.

condition_refs_source_object treats every QuantityCheck containing CountersOn { scope: Source } as a source reference. That is over-broad: Revelation of Power's bare "it" refers to its target creature, so this predicate rebinds the conditional flying/lifelink rider to the Instant itself and drops previously working behavior.

Narrow this to an explicit source noun phrase or suppress the path when the chain already carries the chosen-target referent. Add the Revelation of Power regression test and regenerate/reconcile the parse-diff before requesting review again.

jeffrey701 added a commit to jeffrey701/phase that referenced this pull request Jul 30, 2026
… chosen target (phase-rs#6559 review)

The phase-rs#6507 predicate that binds a source-counter-gated rider pronoun to
SelfRef was one degree too broad: it also fired on Revelation of Power
("Target creature gets +2/+2 until end of turn. If it has a counter on
it, it also gains flying and lifelink"), whose intervening-if mis-scopes
the bare "it" to CountersOn{Source}. Binding that grant to the source
dropped flying/lifelink onto the one-shot Instant — the card lost its
second sentence (engine_regress), and CR 608.2k does not reach it (the
source is named by neither a cost nor a trigger condition).

Narrow the binding: only rebind the pronoun to the source when NO earlier
clause in the chain chose a typed target. Compute one gate at the
chunk-subject site and reuse it at both consumers (the chunk-subject
binding and the replace_target_with_parent guard):

    let binds_source_counter_pronoun = condition
        .is_some_and(condition_refs_source_object)
        && !chain_has_prior_typed_referent(builder.clauses(), false);

chain_has_prior_typed_referent is true for Revelation of Power (its prior
"Target creature gets +2/+2" is a Pump over a typed target) and false for
every depletion-land / counter rider (whose prior clause is "Add mana" or
"put a counter on ~", never a chosen target), so all 21 intended heals
keep SelfRef while Revelation of Power's grant returns to ParentTarget.
Chosen deliberately over chain_prior_referent_is_chosen_target, whose
has_typed_target_widened early-out returns false for a pump-of-a-target.

Adds source_counter_gate_over_prior_target_keeps_parent_not_self_ref
(pins Revelation of Power's grant to ParentTarget) and extends the
predicate unit test to drive the Sum/Difference two-operand walker arms.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jeffrey701

Copy link
Copy Markdown
Contributor Author

Thank you — the Revelation of Power catch was exactly right, and the diagnosis ("a mis-scoped bare it gated by an intervening-if, not a real cost/trigger source reference per CR 608.2k") is the correct framing. Fixed at head 2b3063d8f.

Blocker — Revelation of Power no longer regresses

I took the suppress-when-a-prior-clause-chose-a-typed-target option. At the chunk_subject site (oracle_effect/mod.rs) I compute one gate and reuse it at both consumers (the chunk-subject binding and the replace_target_with_parent guard):

let binds_source_counter_pronoun = condition
    .as_ref()
    .is_some_and(condition_refs_source_object)
    && !chain_has_prior_typed_referent(builder.clauses(), false);

chain_has_prior_typed_referent is true for Revelation of Power — its first clause, "Target creature gets +2/+2", is a Pump { target: Typed(Creature) } (a chosen typed target) — so the source binding is suppressed and the flying/lifelink grant stays on the parent target. Regenerated card-data confirms it:

  • Revelation of Power → grant affected / target = ParentTarget (the flying/lifelink GenericEffect is back on the target creature; the mis-scoped CountersOn{Source} condition is untouched and harmless again).

I chose chain_has_prior_typed_referent over chain_prior_referent_is_chosen_target deliberately: the latter's has_typed_target_widened early-out returns false for a pump-of-a-target, so it would not have caught Revelation of Power. chain_has_prior_typed_referent returns true for the prior typed target and false for the depletion/rider heals (whose prior clause is "Add mana" or "put a counter on ~", never a chosen typed target), so every intended heal is preserved:

rider class prior clause binds source? rider target
Gemstone Mine + 5 depletion lands "Add mana" (no referent) yes SelfRef
Last Light / ED-E / Daredevil Dragster / Blood Spatter Analysis / Charitable Levy / Decree of Silence / The Heron Moon / Contested Game Ball / Dawn of a New Age / Evolved Spinoderm / Tourach's Gate "put/remove a counter on ~" (SelfRef) yes SelfRef
Heirloom Mirror / Ludevic's Test Subject / Replicating Ring / Smoldering Egg "remove a counter from ~" (SelfRef) yes SelfRef (RemoveCounter) ✓
Revelation of Power "Target creature gets +2/+2" (typed target) no ParentTarget

Added source_counter_gate_over_prior_target_keeps_parent_not_self_ref (parser SHAPE test, reach-guarded) pinning Revelation of Power's grant to ParentTarget.

Non-blocking items

  • Claimed set re-derived from the artifact. Corrected list (behavior heals): Gemstone Mine, Peat Bog, Hickory Woodlot, Remote Farm, Sandstone Needle, Saprazzan Skerry, Tourach's Gate, Contested Game Ball, Daredevil Dragster, Dawn of a New Age, Evolved Spinoderm (Sacrifice→SelfRef); Blood Spatter Analysis, Charitable Levy, Decree of Silence, Last Light of Durin's Day, The Heron Moon, ED-E Lonesome Eyebot (trigger Sacrifice/PutCounter→SelfRef); Heirloom Mirror, Ludevic's Test Subject, Replicating Ring, Smoldering Egg (RemoveCounter→SelfRef). Revelation of Power is not in the diff (it stays on main's ParentTarget). The old body's substitution is gone.
  • Blaster, Morale Booster / Hostile Hostel — checked against the regenerated artifact: unchanged by this PR (their transform legs are ParentTarget in all three of main, the previous head, and this head). Blaster's back face ("Move X +1/+1 counters … onto another target artifact. … If Blaster has no +1/+1 counters on it, convert it") is a genuinely harder case: an explicitly-named source gate on a targeted activated ability whose "convert it" wants the source while a target artifact is already chosen. That is precisely the shape this guard excludes (a prior chosen target), and it is a pre-existing main gap, not something this PR introduces or should widen its scope to. It needs the "explicit source noun phrase" discriminator you mentioned (option 1), which is a distinct follow-up — flagging it, out of scope here.
  • Sum / Difference unit coverage — extended condition_refs_source_object_source_counter_quantity_check to drive both two-operand QuantityExpr arms (source read in either operand → detected; neither → not), locking in the exhaustive-walk guarantee.

Full lib + integration suites green; parser-combinator gate (Gate A) passes; the branch is up to date with current main (picks up the Tauri Cargo.lock fix, so the required Rust aggregator is green again).

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested — current-head production coverage gap.

🟠 Required

The repair for the prior Revelation of Power regression still lacks a production-pipeline regression. crates/engine/src/parser/oracle_effect/mod.rs:29929-29948 changes the runtime target binding and :30725-30743 rewrites the parent target, but Revelation is covered only by parser-shape tests in oracle_tests.rs:22924-22981; the new integration cases exercise Gemstone Mine, Peat Bog, and Last Light. The original defect was visible only after casting, target propagation, and layer application, where the grant could be applied to the Instant rather than the selected creature. Add an integration test that casts Revelation of Power at a countered creature and asserts that creature receives flying and lifelink; make it discriminating against reversion of the new guard.

🟡 Also reconcile before re-review

The sole parse-diff artifact predates this head and still describes Revelation as changing to self; the fresh Card data job is in progress. Let it publish and reconcile the exact affected-card/signature set for this head.

Recommendation: add the end-to-end Revelation regression, then provide current-head parse-diff evidence before re-review.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
crates/engine/src/game/engine.rs (1)

9710-9723: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

NeedsChoice(player) shadows the land-playing player passed to the finalizer.

The ReplacementResult::NeedsChoice(player) pattern at line 9710 binds the player who must answer the replacement choice. That binding shadows the outer player resolved at lines 9317-9326, which is the land-playing player (acting_player under shared team turns, otherwise turn_control::turn_resource_owner).

Line 9716 passes the shadowed value to finalize_committed_land_play. The finalizer then attributes the land drop to the chooser: it increments that player's lands_played_this_turn (line 9259) and emits LandPlayed { player_id: player } (lines 9261-9265). When the chooser is not the land-player, the once-per-turn land allowance is charged to the wrong player and the event feed misattributes the play.

The arm needs both identities: line 9725 correctly passes the chooser to replacement_choice_waiting_for. Rename the pattern binding so the two do not collide.

🐛 Proposed fix separating the two identities
-        super::replacement::ReplacementResult::NeedsChoice(player) => {
+        super::replacement::ReplacementResult::NeedsChoice(choosing_player) => {
             // A replacement needs player choice (e.g., shock land "pay 2 life?").
             // Increment counters now — the land play is committed, only the ETB
             // effect is pending.
             finalize_committed_land_play(
                 state,
                 player,
                 object_id,
                 origin_zone,
                 gy_permission_source,
                 exile_play_authorization,
                 library_permission_src,
                 events,
             );
 
             return Ok(super::replacement::replacement_choice_waiting_for(
-                player, state,
+                choosing_player, state,
             ));
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/game/engine.rs` around lines 9710 - 9723, Rename the player
binding in the ReplacementResult::NeedsChoice arm so it does not shadow the
outer land-playing player resolved by the surrounding function. Continue passing
the outer player to finalize_committed_land_play, while passing the renamed
choice-player binding to replacement_choice_waiting_for.
client/src/hooks/__tests__/useConcedeHandler.test.tsx (1)

168-191: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Assert that the unbound path calls neither sendConcede nor dispatch.

The fixture installs { sendConcede: vi.fn() } to model an adapter without the match capability. The test never asserts that this sendConcede stayed uncalled, and it never asserts dispatchMock stayed uncalled.

Those two assertions are the guarantee the change adds: the draft-pod branch must not fall through to a game-level concession. Hold a reference to the mock and assert on it.

🧪 Proposed change
-    adapterForTest = { sendConcede: vi.fn() };
+    const fallbackSendConcede = vi.fn();
+    adapterForTest = { sendConcede: fallbackSendConcede };
     expect(clearGameMock).not.toHaveBeenCalled();
     expect(navigateMock).not.toHaveBeenCalled();
+    expect(fallbackSendConcede).not.toHaveBeenCalled();
+    expect(dispatchMock).not.toHaveBeenCalled();

As per path instructions: "A test must exercise the FAILURE path the fix prevents".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/hooks/__tests__/useConcedeHandler.test.tsx` around lines 168 -
191, Update the unbound draft-pod test around useConcedeHandler to retain the
sendConcede mock reference and assert it is not called after invoking
result.current(). Also assert dispatchMock is not called, preserving the
existing clearGameMock and navigateMock assertions to verify the branch does not
fall through to game-level concession.

Source: Path instructions

client/src/adapter/p2p-adapter.ts (1)

351-369: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Orphaned pendingViews entries stall native revision fan-out.

A revision entry is only deleted when views.size === this.clients.size. detachGuest removes a client from this.clients but leaves that client's partially filled entries in pendingViews. Two consequences follow.

  • A revision that was in flight when a seat detached never reaches the size check again, so onRevision never runs for it. Guests do not receive that state frame.
  • The entry stays in pendingViews for the lifetime of the bridge.

The size comparison is also fragile in the other direction: attachGuest increases clients.size while earlier revisions are still partial.

Prune stale revisions on detach and re-evaluate completeness against the current client set.

🔧 Proposed fix in `detachGuest`
   detachGuest(playerId: PlayerId): void {
     if (playerId === 0) return;
     this.clients.get(playerId)?.dispose();
     this.clients.delete(playerId);
     this.playerTokens.delete(playerId);
     this.latestViews.delete(playerId);
+    // A revision that was still collecting this seat's view can never reach
+    // `clients.size` again. Flush the ones that are now complete and drop the
+    // seat from the rest.
+    for (const [revision, views] of this.pendingViews) {
+      views.delete(playerId);
+      if (views.size === this.clients.size) {
+        this.pendingViews.delete(revision);
+        this.revisionQueue = this.revisionQueue
+          .then(() => this.onRevision(revision, views))
+          .catch((error) => {
+            console.error("[NativeP2PBridge] revision fan-out failed:", error);
+          });
+      }
+    }
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/adapter/p2p-adapter.ts` around lines 351 - 369, Update detachGuest
to remove the detached player’s entries from every pendingViews revision, delete
revisions that become empty, and re-evaluate remaining revisions against the
current clients set so complete revisions invoke onRevision. Ensure attachGuest
or the revision-processing path also rechecks partial revisions against the
current client set, preventing client-count changes from leaving stale or
prematurely blocked fan-out entries.
crates/engine/src/game/effects/mod.rs (1)

5860-5889: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Move the n == 0 guard into drive_sequential_repeated_optional_payment or use saturating_sub.

drive_sequential_repeated_optional_payment computes remaining: (n - 1) as u32. The only thing that keeps n >= 1 is the if n == 0 { return Ok(()) } check that stayed behind in drive_repeated_optional_payment. The new function is now a separate entry point with the arithmetic and the guard in different places. If it is called with n == 0, (0 - 1) as u32 is u32::MAX, and the repeated-payment frame offers effectively unbounded OptionalEffectChoice prompts.

Make the function safe on its own inputs.

🐛 Proposed fix
 fn drive_sequential_repeated_optional_payment(
     state: &mut GameState,
     ability: &ResolvedAbility,
     reflexive: &ResolvedAbility,
     n: i32,
 ) -> Result<(), EffectError> {
+    // CR 603.12a: the payment budget is at least one offer; a zero budget never
+    // opens the process (the caller returns early).
+    let Ok(budget) = u32::try_from(n) else {
+        return Ok(());
+    };
+    let Some(remaining) = budget.checked_sub(1) else {
+        return Ok(());
+    };
-            remaining: (n - 1) as u32,
+            remaining,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/game/effects/mod.rs` around lines 5860 - 5889, Make
drive_sequential_repeated_optional_payment safe when n is zero by returning
Ok(()) before constructing the payment frame, or by using saturating subtraction
for remaining. Keep the existing behavior for positive n and ensure no
zero-count call can create a frame with an effectively unbounded remaining
value.
🟡 Minor comments (11)
crates/phase-ai/src/bin/ai_commander.rs-1050-1070 (1)

1050-1070: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the doc comment: measurement mode is no longer unconditional.

The first paragraph states that EVERY seat runs in measurement mode. build_seat_config now selects the mode from run_context, and RunContext::Interactive keeps the wall-clock deadline. Scope the paragraph to the measurement route so the summary matches the code.

📝 Proposed doc correction
-/// EVERY seat runs in MEASUREMENT mode (`AiConfig::into_measurement`), which
-/// disables the wall-clock search deadline (`AI_SEARCH_TIME_BUDGET_MS`, default
+/// Under `RunContext::Measurement`, every seat runs with
+/// `AiConfig::into_measurement`, which
+/// disables the wall-clock search deadline (`AI_SEARCH_TIME_BUDGET_MS`, default
 /// 1500ms) so search is bounded SOLELY by `max_nodes`/`max_depth`.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/phase-ai/src/bin/ai_commander.rs` around lines 1050 - 1070, Update the
documentation in build_seat_config to scope the measurement-mode behavior to
seats configured through the measurement route, rather than claiming every seat
uses AiConfig::into_measurement. Explicitly preserve that
RunContext::Interactive retains the wall-clock search deadline, while
measurement mode remains bounded by max_nodes/max_depth for reproducibility.
crates/engine/src/game/mana_sources.rs-2388-2394 (1)

2388-2394: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Colorless intrinsic land production stays ungated.

mana_type_to_color returns None for a colorless mana type, so is_some_and yields false and blocked stays false. A land whose only mana source is a granted colorless basic land subtype (Wastes class) therefore bypasses intrinsic_land_mana_ability_blocked entirely, which is the same gate-bypass class this change closes for colored subtypes (issue #6469).

Route the colorless case through the same readiness check, or record in the comment that intrinsic_land_mana_ability_definition accepts only a ManaColor and that colorless intrinsic production is intentionally out of scope.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/game/mana_sources.rs` around lines 2388 - 2394, Update the
blocked calculation near intrinsic land mana handling so colorless mana types
also go through the appropriate readiness gate instead of being skipped when
mana_type_to_color returns None. Reuse intrinsic_land_mana_ability_blocked where
possible, or explicitly document in the surrounding logic that
intrinsic_land_mana_ability_definition only supports ManaColor and colorless
production is intentionally excluded.
crates/server-core/src/game_action_payload_guard.rs-568-570 (1)

568-570: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Rejection messages will name the wrong action for ActivateManaSource.

guard_mana_source_selection_payload hardcodes the "TapLandForMana.selection…" prefix in every error string (lines 207, 212, 217, 222). A hostile ActivateManaSource payload is now rejected with a field path that names an action the client did not send. Pass the action label into the helper so the reason matches the rejected variant.

🔧 Proposed fix
-        GameAction::TapLandForMana { selection } | GameAction::ActivateManaSource { selection } => {
-            guard_mana_source_selection_payload(selection)?;
-        }
+        GameAction::TapLandForMana { selection } => {
+            guard_mana_source_selection_payload("TapLandForMana", selection)?;
+        }
+        GameAction::ActivateManaSource { selection } => {
+            guard_mana_source_selection_payload("ActivateManaSource", selection)?;
+        }

Then build each label from the passed action name inside the helper.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/server-core/src/game_action_payload_guard.rs` around lines 568 - 570,
Update guard_mana_source_selection_payload and its call site in the GameAction
match to accept the action label, passing the appropriate label for
TapLandForMana or ActivateManaSource. Build every rejection field path inside
the helper from that label instead of hardcoding “TapLandForMana.selection”, so
each error identifies the actual rejected action.
client/src/components/multiplayer/ConcedeDialog.tsx-58-75 (1)

58-75: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Raise the button height to meet the 44pt touch target.

px-5 py-2 text-sm produces roughly a 36px tall control. The path instructions require touch targets of at least 44pt. Both changed buttons use this padding.

Add min-h-11 (44px) and center the label.

🛠️ Proposed change
               <button
                 onClick={gameAction.onConfirm}
-                className="rounded-lg bg-red-600 px-5 py-2 text-sm font-semibold text-white transition hover:bg-red-500"
+                className="min-h-11 rounded-lg bg-red-600 px-5 py-2 text-sm font-semibold text-white transition hover:bg-red-500"
               >

Apply the same min-h-11 to the match button at line 71.

As per path instructions: "Touch targets >= 44pt".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/components/multiplayer/ConcedeDialog.tsx` around lines 58 - 75,
Update both the game and match confirmation buttons in ConcedeDialog to include
min-h-11 and vertically center their labels, preserving their existing styling
and behavior.

Source: Path instructions

client/src/adapter/__tests__/p2pDraftHostBo3.test.ts-437-445 (1)

437-445: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

This test exercises the !pairing branch, not the round-currency branch.

binding.round is 2 (line 340). setHostView supplies current_round: 2, so view.current_round !== binding.round is false. The pairing lookup in acceptMatchSettlement matches on match_id and candidate.round === binding.round, and the fixture supplies pairing("m-12", 1, 1, 2) with round 1. The lookup therefore returns undefined and the rejection comes from !pairing.

The view.current_round !== binding.round guard stays untested. Add a case that keeps the pairing at binding.round and advances current_round.

🧪 Proposed added case
+    it("rejects a bound settlement once the pod advanced past its round", async () => {
+      setHostView({
+        current_round: 3,
+        pairings: [pairing("m-12", 2, 1, 2)],
+      });
+      await deliverSettlement(1);
+      expect(reportSpy).not.toHaveBeenCalled();
+      expect(sent.get(1)).toEqual([
+        { type: "draft_error", reason: "Unauthorized match settlement" },
+      ]);
+    });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/adapter/__tests__/p2pDraftHostBo3.test.ts` around lines 437 - 445,
Update the test around the settlement rejection case so the pairing returned by
pairing("m-12", ...) uses binding.round (2), while setHostView.current_round is
advanced to a different round (for example, 3). Keep the settlement delivery and
rejection assertions, ensuring acceptMatchSettlement exercises the
view.current_round !== binding.round guard rather than the !pairing branch.
client/src/network/protocol.ts-95-110 (1)

95-110: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Reconcile the duplicated version-17 log entries.

The version log now has two separate 17 headings: line 95 for sacrificial-mana source selection, and line 103 for bound draft-match concession. The second one is also placed between the 7 and 6 entries, which breaks the descending order and hides it from a reader scanning the top of the list. WIRE_PROTOCOL_VERSION is a single integer, so both changes ship as one version and must be described in one entry.

This version also adds the authority stamp, sessionKey on reconnect, revision on game_setup/state_update/reconnect_ack, and the terminal_result frame. None of those appear in the log. A future author bumping to 18 can miss one of the two 17 blocks.

📝 Proposed consolidation
- *  17 — Sacrificial-mana source selection action and waiting-state snapshots.
+ *  17 — Sacrificial-mana source selection action and waiting-state snapshots;
+ *       host authority stamp on every host-originated frame; reconnect
+ *       sessionKey; state revisions on game_setup/state_update/reconnect_ack;
+ *       recipient-scoped terminal_result frames; bound draft-match concession
+ *       request (a Traditional-draft guest asks its match authority to settle
+ *       the match instead of sending a game-level concession).
  *  12 — Connive exact subject snapshots and resident paused post-replacement
  *       drains changed P2P GameState snapshots.
  *  11 — Serialized GameState trigger provenance and paused logical zone-change owners.
  *  10 — Dedicated companion deck slot and typed companion-reveal choices.
  *   9 — Meld pair and attacking-entry choices after mana-payment preview variants.
  *   8 — Mana-payment preview request/response variants.
  *   7 — PrecastCopyShortcut action and its two WaitingFor variants.
- *  17 — Bound draft-match concession request. A Traditional-draft guest
- *       asks its match authority to settle the match; it must not send a
- *       game-level concession through the ordinary P2P path.
  *   6 — Mulligan bottoming folded into a MulliganDecisionPhase::BottomCards
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/network/protocol.ts` around lines 95 - 110, Consolidate the
duplicate version-17 entries in the protocol version history comment into one
top-level entry, preserving descending order and covering all version-17
changes: sacrificial-mana source selection, bound draft-match concession,
authority stamping, reconnect sessionKey, revision fields, and the
terminal_result frame. Keep WIRE_PROTOCOL_VERSION unchanged at 17.
crates/engine/src/game/match_flow.rs-295-313 (1)

295-313: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

A forfeit by a player who already holds two game wins produces a 2-2 frozen score.

The guards accept any state whose match_phase is not Completed. If p0_wins is already 2 while the phase is still InGame — the window before handle_game_over_transition runs for the clinching game — and PlayerId(0) forfeits, Line 301 sets p1_wins = 2. The recorded match_score becomes 2-2 while match_forfeit_result.winner is PlayerId(1), and that inconsistent score is what terminal presentation freezes and shows.

Reject the forfeit when the opponent has already clinched the match, so the earned result is never overwritten.

🐛 Proposed fix
     let winner = match forfeiting_player {
         PlayerId(0) => PlayerId(1),
         PlayerId(1) => PlayerId(0),
         _ => return Err("Forfeiting player is not a match seat".to_string()),
     };
+    // A seat that has already won the match cannot hand it to its opponent:
+    // clamping the opponent to two wins would record a 2-2 score.
+    let forfeiting_wins = match forfeiting_player {
+        PlayerId(0) => state.match_score.p0_wins,
+        _ => state.match_score.p1_wins,
+    };
+    if forfeiting_wins >= 2 {
+        return Err("Forfeiting player has already won the match".to_string());
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/game/match_flow.rs` around lines 295 - 313, Update the
forfeit handling before the score mutation in the match transition flow to
reject a forfeit when the forfeiting player’s opponent already has the clinching
game count (two wins), even if match_phase is not yet Completed. Preserve the
existing score and winner state in that case, and only execute the
match_forfeit_result and frozen-score updates for valid forfeits.
crates/phase-server/src/persistence.rs-403-423 (1)

403-423: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

The activation upsert accepts an equal generation and can overwrite a live snapshot with revision 0.

The guard at Line 414 is excluded.generation >= game_sessions.generation. The earlier check at Line 391 only rejects a strictly greater stored generation. An activation for the same game_code and the same generation therefore passes both checks and replaces session_json and mutation_revision with the freshly created values, discarding the retained state of the row already at that generation.

create_full_session_key refuses to allocate while a non-retired row exists, so the path is not reachable today. Rated by effect when reached, this is snapshot loss for an active session. Make the predicate strict, and let the caller treat a non-Applied disposition as the conflict it is.

🐛 Proposed fix
-             WHERE excluded.generation >= game_sessions.generation",
+             WHERE excluded.generation > game_sessions.generation
+                OR game_sessions.retired = 1",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/phase-server/src/persistence.rs` around lines 403 - 423, Update the
conflict predicate in the game-session activation upsert to require
excluded.generation to be strictly greater than game_sessions.generation,
preventing equal-generation snapshots from overwriting retained state. In the
surrounding activation caller, handle any disposition other than Applied as a
conflict, preserving existing behavior for successfully applied activations.
crates/engine/src/parser/oracle_tests.rs-23286-23320 (1)

23286-23320: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add the reach-guards this test's three siblings carry.

explicit_source_counter_gate_over_prior_target_stays_source_scoped asserts a scope value without proving the rider reached the code under test. Its siblings depletion_land_rider_sacrifice_binds_self_ref, typed_trigger_source_counter_rider_binds_self_ref_not_triggering_source, and source_counter_gate_over_prior_target_keeps_parent_not_self_ref all assert zero parse warnings and zero Effect::Unimplemented first.

Without those guards, a rider that misparsed into an Unimplemented effect while still carrying a CountersOn { scope: Source } condition satisfies the matches! and the test passes for the wrong reason. This test is the discriminating half of the pair with the Revelation of Power guard, so a false green here removes the only evidence that the suppression keys on the bare anaphor rather than on "a prior typed target exists".

💚 Proposed reach-guards and a positive rider-shape assertion
     );
 
+    assert!(
+        parsed.parse_warnings.is_empty(),
+        "expected zero parse warnings, got {:#?}",
+        parsed.parse_warnings
+    );
+    fn has_unimpl(def: &AbilityDefinition) -> bool {
+        matches!(def.effect.as_ref(), Effect::Unimplemented { .. })
+            || def.sub_ability.as_deref().is_some_and(has_unimpl)
+    }
+    assert!(
+        !parsed.abilities.iter().any(has_unimpl),
+        "no Unimplemented anywhere in the parse: {:#?}",
+        parsed.abilities
+    );
     let rider = parsed.abilities[0]
         .sub_ability
         .as_deref()
         .expect("conditional flying rider");
+    assert!(
+        matches!(rider.effect.as_ref(), Effect::GenericEffect { .. }),
+        "the rider must remain the flying grant, got {:?}",
+        rider.effect
+    );
     assert!(

As per path instructions: "For every negative assertion … require a paired positive reach-guard proving the input actually reached the code under test (parse succeeded, zero Effect::Unimplemented, expected positive shape); an upstream short-circuit makes a negative pass for the wrong reason."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/parser/oracle_tests.rs` around lines 23286 - 23320, Update
explicit_source_counter_gate_over_prior_target_stays_source_scoped to add reach
guards before the scope match: assert zero parse warnings and zero
Effect::Unimplemented effects, then assert the rider’s expected positive shape
before checking CountersOn with ObjectScope::Source. Mirror the guard pattern
used by depletion_land_rider_sacrifice_binds_self_ref,
typed_trigger_source_counter_rider_binds_self_ref_not_triggering_source, and
source_counter_gate_over_prior_target_keeps_parent_not_self_ref.

Source: Path instructions

crates/engine/tests/integration/issue_4956_gift_of_immortality_reattach.rs-982-993 (1)

982-993: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Accepting Effect::Unimplemented makes this assertion non-discriminating.

The match arm at Line 987 passes when the leave-battlefield rider is completely unmodeled. The test then reports success for both the fixed shape (AddTargetReplacement{TrackedSet}) and the unfixed shape. Assert the intended shape only, or split the tolerance into a separate #[ignore]d or explicitly documented expected-gap test so a regression to Unimplemented fails.

As per path instructions: "Flag constructor shortcuts … that can silently mask the very bug a regression test claims to catch."

🧪 Proposed tightening
     match find_leave_rider(execute) {
         Some(Effect::AddTargetReplacement {
             target: TargetFilter::TrackedSet { .. },
             ..
         }) => {}
-        Some(Effect::Unimplemented { .. }) => {}
         Some(other) => panic!(
-            "leave-battlefield rider must be AddTargetReplacement{{TrackedSet}} or \
-             Unimplemented, got {other:?}"
+            "leave-battlefield rider must be AddTargetReplacement{{TrackedSet}}, got {other:?}"
         ),
         None => panic!("expected leave-battlefield rider in Storm Herald chain"),
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/tests/integration/issue_4956_gift_of_immortality_reattach.rs`
around lines 982 - 993, Update the leave-battlefield rider assertion around
find_leave_rider so only Effect::AddTargetReplacement with
TargetFilter::TrackedSet is accepted. Remove the successful
Effect::Unimplemented match arm, ensuring an unmodeled rider reaches the
existing failure path and the regression test cannot pass for the unfixed
behavior.

Source: Path instructions

crates/engine/src/game/casting_tests.rs-31223-31317 (1)

31223-31317: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Pair the three sibling-gate negatives with a positive reach-guard that uses add_bare_subtype_forest.

bare_subtype_land_detained_excluded_from_legal_mana_actions, bare_subtype_land_phased_out_excluded_from_legal_mana_actions, and bare_subtype_land_cant_tap_excluded_from_legal_mana_actions assert only absence from activatable_mana_actions_for_player. All three build their land through add_bare_subtype_forest. The positive companion bare_subtype_land_still_offers_mana_without_a_prohibition builds its land inline instead of calling that helper. If the helper ever stops producing a valid intrinsic mana source (for example a changed subtype string or a missing entered_battlefield_turn), all three negatives pass for the wrong reason and the positive test still passes.

Add a positive assertion inside each gate test before applying the gate, or route the positive companion through the same helper.

💚 Proposed reach-guard inside one gate test
     let mut state = setup_game_at_main_phase();
     let forest = add_bare_subtype_forest(&mut state, PlayerId(1), 0xF0128);
+    assert!(
+        crate::game::mana_sources::activatable_mana_actions_for_player(&state, PlayerId(1))
+            .iter()
+            .any(|action| action.source_object() == Some(forest)),
+        "reach-guard: the helper-built land must offer its intrinsic mana ability before the gate"
+    );
     state
         .objects
         .get_mut(&forest)

As per path instructions: "For every negative assertion (!detector(...), "not applied", "does not parse to X"), require a paired positive reach-guard proving the input actually reached the code under test".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/game/casting_tests.rs` around lines 31223 - 31317, Ensure
the three negative tests using add_bare_subtype_forest first verify that the
unmodified forest appears in activatable_mana_actions_for_player, before
applying detention, phasing, or CantTap. Alternatively, update
bare_subtype_land_still_offers_mana_without_a_prohibition to construct the land
through add_bare_subtype_forest, so the shared helper is positively proven to
produce an intrinsic mana source before each prohibition assertion.

Source: Path instructions


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 680aa6e7-c6ab-4b66-8a2c-48e2dfdb2da6

📥 Commits

Reviewing files that changed from the base of the PR and between 11b490a and ae167b1.

⛔ Files ignored due to path filters (66)
  • Cargo.lock is excluded by !**/*.lock
  • client/public/changelog-meta.json is excluded by !client/public/changelog*.json
  • client/public/changelog.json is excluded by !client/public/changelog*.json
  • client/src-tauri/Cargo.lock is excluded by !**/*.lock
  • client/src/adapter/generated/interaction/index.ts is excluded by !**/generated/**
  • client/src/wasm/engine_wasm.d.ts is excluded by !client/src/wasm/**, !**/*.d.ts
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__aangs_journey_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__abraxas_named_equip_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__abraxas_named_equip_lowered.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__aerial_formation_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__aetherling_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__analyze_the_pollen_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__arni_brokenbrow_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__barbarian_ring_activated_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__batterskull_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__birds_of_paradise_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__blunt_the_assault_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__bomat_courier_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__bone_splinters_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__boseiju_who_endures_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__browbeat_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__carbonize_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__case_of_the_stashed_skeleton_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__champions_victory_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__chandra_nalaar_minus_x_loyalty_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__chandra_nalaar_minus_x_loyalty_lowered.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__component_pouch_activated_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__conformer_shuriken_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__deadly_rollick_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__dismember_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__evils_thrall_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__experiment_one_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__figure_of_destiny_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__fog_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__follow_the_lumarets_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__ghost_lit_stalker_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__govern_the_guildless_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__guul_draz_assassin_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__incinerate_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__jace_the_mind_sculptor_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__jade_mage_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__joraga_treespeaker_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__liliana_of_the_veil_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__liliana_the_repentant_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__llanowar_elves_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__manamorphose_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__mother_of_runes_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__questing_beast_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__repeat_offender_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__short_sword_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__stoneforge_mystic_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__swords_to_plowshares_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__sylvan_safekeeper_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__temporal_delayed_trigger@full_throttle_temporal_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__temporal_delayed_trigger@full_throttle_temporal_lowered.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__temporal_delayed_trigger@galvanic_iteration_temporal_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__temporal_delayed_trigger@galvanic_iteration_temporal_lowered.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__temporal_delayed_trigger@pact_of_negation_temporal_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__temporal_delayed_trigger@pact_of_negation_temporal_lowered.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__thespians_stage_generic_activated_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__touch_of_the_void_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__village_rites_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__vines_of_vastwood_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__walking_ballista_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/tests/fixtures/cr733/authority_matrix.json.gz is excluded by !**/*.gz
  • lobby-worker/broker-wasm/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (239)
  • .github/workflows/ai-gate.yml
  • Cargo.toml
  • Tiltfile
  • client/package.json
  • client/public/feeds/mtggoldfish-commander.json
  • client/public/feeds/mtggoldfish-modern.json
  • client/public/feeds/mtggoldfish-pioneer.json
  • client/public/feeds/mtggoldfish-standard.json
  • client/src-tauri/Cargo.toml
  • client/src-tauri/tauri.conf.json
  • client/src/adapter/__tests__/ai-card-subset.test.ts
  • client/src/adapter/__tests__/ai-worker-pool.test.ts
  • client/src/adapter/__tests__/manaSourceSelectionWireTypes.test.ts
  • client/src/adapter/__tests__/p2p-adapter-multiplayer.test.ts
  • client/src/adapter/__tests__/p2pDraftHostBo3.test.ts
  • client/src/adapter/__tests__/wasm-adapter.test.ts
  • client/src/adapter/__tests__/ws-adapter.test.ts
  • client/src/adapter/ai-worker-pool.ts
  • client/src/adapter/card-db-subset.ts
  • client/src/adapter/draftPodGuestAdapter.ts
  • client/src/adapter/draftPodHostAdapter.ts
  • client/src/adapter/engine-worker-client.ts
  • client/src/adapter/engine-worker.ts
  • client/src/adapter/p2p-adapter.ts
  • client/src/adapter/p2p-draft-guest.ts
  • client/src/adapter/p2p-draft-host.ts
  • client/src/adapter/replay-adapter.ts
  • client/src/adapter/server-draft-adapter.ts
  • client/src/adapter/types.ts
  • client/src/adapter/wasm-adapter.ts
  • client/src/adapter/ws-adapter.ts
  • client/src/components/board/__tests__/PermanentCard.test.tsx
  • client/src/components/mana/ManaPaymentUI.tsx
  • client/src/components/mana/__tests__/ManaPaymentUI.test.tsx
  • client/src/components/modal/__tests__/ModeChoiceModal.test.tsx
  • client/src/components/modal/__tests__/TriggerOrderModal.test.tsx
  • client/src/components/multiplayer/ConcedeDialog.tsx
  • client/src/components/multiplayer/__tests__/ConcedeDialog.test.tsx
  • client/src/components/settings/PreferencesModal.tsx
  • client/src/game/__tests__/castPaymentMode.test.ts
  • client/src/game/__tests__/dispatchSplitEpochSoftlock.test.ts
  • client/src/game/__tests__/dispatchTurnControlPayCostQueue.test.ts
  • client/src/game/castPaymentMode.ts
  • client/src/game/controllers/__tests__/aiController.test.ts
  • client/src/game/controllers/aiController.ts
  • client/src/game/dispatch.ts
  • client/src/game/waitingForRegistry.ts
  • client/src/hooks/__tests__/useConcedeHandler.test.tsx
  • client/src/hooks/__tests__/useKeyboardShortcuts.test.tsx
  • client/src/hooks/useConcedeHandler.ts
  • client/src/i18n/locales/de/game.json
  • client/src/i18n/locales/de/multiplayer.json
  • client/src/i18n/locales/de/settings.json
  • client/src/i18n/locales/en/game.json
  • client/src/i18n/locales/en/multiplayer.json
  • client/src/i18n/locales/en/settings.json
  • client/src/i18n/locales/es/game.json
  • client/src/i18n/locales/es/multiplayer.json
  • client/src/i18n/locales/es/settings.json
  • client/src/i18n/locales/fr/game.json
  • client/src/i18n/locales/fr/multiplayer.json
  • client/src/i18n/locales/fr/settings.json
  • client/src/i18n/locales/it/game.json
  • client/src/i18n/locales/it/multiplayer.json
  • client/src/i18n/locales/it/settings.json
  • client/src/i18n/locales/pl/game.json
  • client/src/i18n/locales/pl/multiplayer.json
  • client/src/i18n/locales/pl/settings.json
  • client/src/i18n/locales/pt/game.json
  • client/src/i18n/locales/pt/multiplayer.json
  • client/src/i18n/locales/pt/settings.json
  • client/src/network/__tests__/draftProtocol.test.ts
  • client/src/network/__tests__/protocol.test.ts
  • client/src/network/draftProtocol.ts
  • client/src/network/protocol.ts
  • client/src/pages/DraftPodPage.tsx
  • client/src/pages/GamePage.tsx
  • client/src/pages/__tests__/DraftPodPage.betweenGames.test.tsx
  • client/src/pages/__tests__/GamePage.bracketViolation.test.tsx
  • client/src/pages/__tests__/greenwardenDoubledTrigger.test.ts
  • client/src/pages/__tests__/optionalEffectChoiceTransition.test.tsx
  • client/src/providers/GameProvider.tsx
  • client/src/providers/__tests__/GameProvider.nativeEngine.test.tsx
  • client/src/services/__tests__/draftPersistence.test.ts
  • client/src/services/__tests__/fullTerminalResult.test.ts
  • client/src/services/__tests__/gamePersistence.test.ts
  • client/src/services/__tests__/intergameCommandLedger.test.ts
  • client/src/services/__tests__/multiplayerSession.test.ts
  • client/src/services/__tests__/p2pSession.test.ts
  • client/src/services/__tests__/p2pTerminalResult.test.ts
  • client/src/services/__tests__/scryfall.test.ts
  • client/src/services/draftPersistence.ts
  • client/src/services/fullTerminalResult.ts
  • client/src/services/gamePersistence.ts
  • client/src/services/intergameCommandLedger.ts
  • client/src/services/multiplayerSession.ts
  • client/src/services/p2pSession.ts
  • client/src/services/p2pTerminalResult.ts
  • client/src/stores/__tests__/multiplayerDraftStore.test.ts
  • client/src/stores/__tests__/multiplayerStore.test.ts
  • client/src/stores/multiplayerDraftStore.ts
  • client/src/stores/multiplayerStore.ts
  • client/src/stores/preferencesStore.ts
  • client/src/test/factories/engineAdapterFactory.ts
  • client/src/viewmodel/__tests__/cardActionChoice.test.ts
  • crates/engine-wasm/src/lib.rs
  • crates/engine/data/mtgjson-vintage
  • crates/engine/src/ai_support/candidates.rs
  • crates/engine/src/ai_support/context.rs
  • crates/engine/src/ai_support/filter.rs
  • crates/engine/src/ai_support/mod.rs
  • crates/engine/src/ai_support/payment_continuation.rs
  • crates/engine/src/ai_support/targeted_exchange.rs
  • crates/engine/src/game/ability_scan.rs
  • crates/engine/src/game/ability_utils.rs
  • crates/engine/src/game/casting.rs
  • crates/engine/src/game/casting_costs.rs
  • crates/engine/src/game/casting_tests.rs
  • crates/engine/src/game/costs.rs
  • crates/engine/src/game/coverage.rs
  • crates/engine/src/game/derived.rs
  • crates/engine/src/game/effects/cast_from_zone.rs
  • crates/engine/src/game/effects/change_zone.rs
  • crates/engine/src/game/effects/deal_damage.rs
  • crates/engine/src/game/effects/delayed_trigger.rs
  • crates/engine/src/game/effects/exile_from_top_until.rs
  • crates/engine/src/game/effects/free_cast_from_zones.rs
  • crates/engine/src/game/effects/mod.rs
  • crates/engine/src/game/effects/pay.rs
  • crates/engine/src/game/effects/prepare.rs
  • crates/engine/src/game/effects/put_on_top.rs
  • crates/engine/src/game/effects/token.rs
  • crates/engine/src/game/engine.rs
  • crates/engine/src/game/engine_modes.rs
  • crates/engine/src/game/engine_resolution_choices.rs
  • crates/engine/src/game/engine_resolve_batch.rs
  • crates/engine/src/game/interaction.rs
  • crates/engine/src/game/mana_abilities.rs
  • crates/engine/src/game/mana_sources.rs
  • crates/engine/src/game/marksman_tests.rs
  • crates/engine/src/game/match_flow.rs
  • crates/engine/src/game/replay.rs
  • crates/engine/src/game/scenario.rs
  • crates/engine/src/game/visibility.rs
  • crates/engine/src/game/zone_pipeline.rs
  • crates/engine/src/game/zones.rs
  • crates/engine/src/parser/oracle.rs
  • crates/engine/src/parser/oracle_class.rs
  • crates/engine/src/parser/oracle_effect/assembly.rs
  • crates/engine/src/parser/oracle_effect/conditions.rs
  • crates/engine/src/parser/oracle_effect/imperative.rs
  • crates/engine/src/parser/oracle_effect/lower.rs
  • crates/engine/src/parser/oracle_effect/mod.rs
  • crates/engine/src/parser/oracle_effect/sequence.rs
  • crates/engine/src/parser/oracle_effect/subject.rs
  • crates/engine/src/parser/oracle_effect/tests.rs
  • crates/engine/src/parser/oracle_effect/token.rs
  • crates/engine/src/parser/oracle_ir/ast.rs
  • crates/engine/src/parser/oracle_ir/context.rs
  • crates/engine/src/parser/oracle_ir/doc.rs
  • crates/engine/src/parser/oracle_ir/effect_chain.rs
  • crates/engine/src/parser/oracle_ir/feature.rs
  • crates/engine/src/parser/oracle_ir/relation.rs
  • crates/engine/src/parser/oracle_ir/snapshot_tests.rs
  • crates/engine/src/parser/oracle_ir/trigger.rs
  • crates/engine/src/parser/oracle_modal.rs
  • crates/engine/src/parser/oracle_nom/condition.rs
  • crates/engine/src/parser/oracle_separate_piles.rs
  • crates/engine/src/parser/oracle_special.rs
  • crates/engine/src/parser/oracle_static/keyword_grant.rs
  • crates/engine/src/parser/oracle_tests.rs
  • crates/engine/src/parser/oracle_trigger.rs
  • crates/engine/src/parser/oracle_trigger_tests.rs
  • crates/engine/src/parser/oracle_util.rs
  • crates/engine/src/types/ability.rs
  • crates/engine/src/types/action_stable_order.rs
  • crates/engine/src/types/actions.rs
  • crates/engine/src/types/game_state.rs
  • crates/engine/src/types/interaction.rs
  • crates/engine/src/types/mana.rs
  • crates/engine/src/types/match_config.rs
  • crates/engine/src/types/mod.rs
  • crates/engine/src/types/resolution.rs
  • crates/engine/tests/integration/cr733_resolved_frame_transition.rs
  • crates/engine/tests/integration/diluvian_primordial_6754.rs
  • crates/engine/tests/integration/gemstone_mine_depletion_sacrifice_6507.rs
  • crates/engine/tests/integration/invoke_calamity_free_cast.rs
  • crates/engine/tests/integration/issue_4956_gift_of_immortality_reattach.rs
  • crates/engine/tests/integration/issue_6477_wandering_archaic_optional_payment.rs
  • crates/engine/tests/integration/issue_6677_wakandan_royal_guard.rs
  • crates/engine/tests/integration/jagged_lightning_each_of_two_targets.rs
  • crates/engine/tests/integration/main.rs
  • crates/engine/tests/integration/riptide_gearhulk_5994.rs
  • crates/engine/tests/integration/sacrificial_mana_choice.rs
  • crates/engine/tests/integration/self_destruct_target_power.rs
  • crates/engine/tests/integration/undying_malice_edict_sacrifice_5942.rs
  • crates/manabrew-compat/src/lib.rs
  • crates/mtgish-import/src/convert/mod.rs
  • crates/phase-ai/Cargo.toml
  • crates/phase-ai/baselines/perf-baseline.json
  • crates/phase-ai/src/auto_play.rs
  • crates/phase-ai/src/bin/ai_bench_state.rs
  • crates/phase-ai/src/bin/ai_commander.rs
  • crates/phase-ai/src/bin/ai_duel.rs
  • crates/phase-ai/src/bin/ai_gate.rs
  • crates/phase-ai/src/bin/ai_perf_gate.rs
  • crates/phase-ai/src/bin/ai_tune.rs
  • crates/phase-ai/src/bin/attack_scaling_bench.rs
  • crates/phase-ai/src/bin/combat_priority_bench.rs
  • crates/phase-ai/src/bin/declare_attackers_bench.rs
  • crates/phase-ai/src/bin/legal_actions_bench.rs
  • crates/phase-ai/src/bin/pass_priority_bench.rs
  • crates/phase-ai/src/bin/resolve_bench.rs
  • crates/phase-ai/src/decision_kind.rs
  • crates/phase-ai/src/duel_suite/perf.rs
  • crates/phase-ai/src/mana_colors.rs
  • crates/phase-ai/src/policies/discard_payoff.rs
  • crates/phase-ai/src/policies/draw_payoff.rs
  • crates/phase-ai/src/policies/self_cost.rs
  • crates/phase-ai/src/policies/self_cost_value.rs
  • crates/phase-ai/src/search.rs
  • crates/phase-ai/src/tactical_gate.rs
  • crates/phase-ai/tests/ai_commander_batch_equivalence.rs
  • crates/phase-server/src/main.rs
  • crates/phase-server/src/persistence.rs
  • crates/server-core/src/client_message_wire_guard.rs
  • crates/server-core/src/game_action_payload_guard.rs
  • crates/server-core/src/lib.rs
  • crates/server-core/src/p2p_backup_guard.rs
  • crates/server-core/src/protocol.rs
  • crates/server-core/src/reconnect.rs
  • crates/server-core/src/session.rs
  • crates/server-core/tests/game_action_payload_guard.rs
  • crates/server-core/tests/lobby_wire_contract.rs
  • docs/parser-misparse-backlog.md
  • scripts/changelog/state.json
  • scripts/gen-scryfall-sets.sh
  • scripts/lib/scryfall-fetch.sh
  • scripts/prelowered-ratchet.txt
💤 Files with no reviewable changes (6)
  • client/src/adapter/tests/wasm-adapter.test.ts
  • client/src/adapter/replay-adapter.ts
  • client/src/pages/tests/greenwardenDoubledTrigger.test.ts
  • client/src/pages/tests/optionalEffectChoiceTransition.test.tsx
  • client/src/adapter/server-draft-adapter.ts
  • crates/mtgish-import/src/convert/mod.rs

Comment thread crates/engine/src/game/engine.rs
Comment thread crates/server-core/src/session.rs
matthewevans pushed a commit to jeffrey701/phase that referenced this pull request Aug 1, 2026
… chosen target (phase-rs#6559 review)

The phase-rs#6507 predicate that binds a source-counter-gated rider pronoun to
SelfRef was one degree too broad: it also fired on Revelation of Power
("Target creature gets +2/+2 until end of turn. If it has a counter on
it, it also gains flying and lifelink"), whose intervening-if mis-scopes
the bare "it" to CountersOn{Source}. Binding that grant to the source
dropped flying/lifelink onto the one-shot Instant — the card lost its
second sentence (engine_regress), and CR 608.2k does not reach it (the
source is named by neither a cost nor a trigger condition).

Narrow the binding: only rebind the pronoun to the source when NO earlier
clause in the chain chose a typed target. Compute one gate at the
chunk-subject site and reuse it at both consumers (the chunk-subject
binding and the replace_target_with_parent guard):

    let binds_source_counter_pronoun = condition
        .is_some_and(condition_refs_source_object)
        && !chain_has_prior_typed_referent(builder.clauses(), false);

chain_has_prior_typed_referent is true for Revelation of Power (its prior
"Target creature gets +2/+2" is a Pump over a typed target) and false for
every depletion-land / counter rider (whose prior clause is "Add mana" or
"put a counter on ~", never a chosen target), so all 21 intended heals
keep SelfRef while Revelation of Power's grant returns to ParentTarget.
Chosen deliberately over chain_prior_referent_is_chosen_target, whose
has_typed_target_widened early-out returns false for a pump-of-a-target.

Adds source_counter_gate_over_prior_target_keeps_parent_not_self_ref
(pins Revelation of Power's grant to ParentTarget) and extends the
predicate unit test to drive the Sum/Difference two-operand walker arms.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@matthewevans
matthewevans force-pushed the fix/6507-depletion-sacrifice-rider branch from ae167b1 to cf2e109 Compare August 1, 2026 17:26

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
crates/engine/src/parser/oracle_tests.rs (3)

23059-23062: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the duplicated has_unimpl helper.

fn has_unimpl is defined identically three times in this file: at Line 23059, Line 23133, and Line 23222. Move it to a single module-level helper function and call it from each test.

♻️ Proposed refactor
+fn ability_has_unimpl(def: &AbilityDefinition) -> bool {
+    matches!(def.effect.as_ref(), Effect::Unimplemented { .. })
+        || def.sub_ability.as_deref().is_some_and(ability_has_unimpl)
+}
+
 #[test]
 fn depletion_land_rider_sacrifice_binds_self_ref() {
     ...
-    fn has_unimpl(def: &AbilityDefinition) -> bool {
-        matches!(def.effect.as_ref(), Effect::Unimplemented { .. })
-            || def.sub_ability.as_deref().is_some_and(has_unimpl)
-    }
     assert!(
-        !parsed.abilities.iter().any(has_unimpl),
+        !parsed.abilities.iter().any(ability_has_unimpl),
         "no Unimplemented anywhere in the parse: {:#?}",
         parsed.abilities
     );

Also applies to: 23133-23136, 23222-23225

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/parser/oracle_tests.rs` around lines 23059 - 23062, Extract
the duplicated has_unimpl helper into one module-level function in
oracle_tests.rs, then remove the three local definitions and reuse the shared
helper from each affected test. Preserve its existing recursive check for
Effect::Unimplemented and nested sub_ability values.

23278-23315: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add reach-guards to this test, matching its siblings.

This test does not assert parsed.parse_warnings.is_empty() or check for Effect::Unimplemented before asserting the HasCounters shape, unlike depletion_land_rider_sacrifice_binds_self_ref (Line 23054-23067), typed_trigger_source_counter_rider_binds_self_ref_not_triggering_source (Line 23128-23145), and source_counter_gate_over_prior_target_keeps_parent_not_self_ref (Line 23217-23230). Without these guards, a degraded parse that still happens to produce a GenericEffect/HasCounters shape through a different, unintended path would pass this test for the wrong reason. This test protects the same anaphora-binding logic that a prior review already flagged as a regression source (Revelation of Power), so the same reach-guard rigor applies here.

✅ Proposed fix
     let rider = parsed.abilities[0]
         .sub_ability
         .as_deref()
         .expect("conditional flying rider");
+    assert!(
+        parsed.parse_warnings.is_empty(),
+        "expected zero parse warnings, got {:#?}",
+        parsed.parse_warnings
+    );
     let Effect::GenericEffect {
         static_abilities, ..
     } = rider.effect.as_ref()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/parser/oracle_tests.rs` around lines 23278 - 23315,
Strengthen explicit_source_counter_gate_over_prior_target_stays_source_scoped
with the same reach guards as its sibling tests: assert parsed.parse_warnings is
empty and verify the relevant parsed effect is not Effect::Unimplemented before
inspecting its GenericEffect/HasCounters structure. Keep the existing semantic
assertions unchanged.

23278-23281: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fix the CR 608.2c citation.

The comment cites "CR 122.1 + CR 608.2c" to justify that a prior typed target does not rewrite an explicit source subject. CR 608.2c reads: "The controller of the spell or ability follows its instructions in the order written." That rule governs order-of-resolution, not anaphor/subject binding. Cite a rule that actually supports subject-scope preservation (for example, CR 608.2k already used correctly elsewhere in this file for anaphora to a cost/trigger-named object) instead of CR 608.2c here.

Based on learnings, cite "CR 608.2c only when the comment is documenting the resolution of written instructions “in order” (not for general keyword-list behavior)."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/parser/oracle_tests.rs` around lines 23278 - 23281, Update
the test comment above the affected test to replace the incorrect CR 608.2c
citation with the applicable subject/anaphora-scope rule, such as CR 608.2k,
while retaining CR 122.1 if relevant. Use CR 608.2c only for comments describing
resolution of written instructions in order, not subject binding or keyword-list
behavior.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/engine/src/parser/oracle_effect/tests.rs`:
- Around line 29364-29370: Remove the “CR 608.2k” citation from the issue
comment in the test documentation, leaving the Issue `#6507` and CR 122.1
references and the existing explanation of source-scoped versus
target/recipient-scoped counter checks unchanged.
- Around line 29392-29406: Extend the tests around condition_refs_source_object
with a positive QuantityCheck case that places CountersOn { scope:
ObjectScope::Source } in rhs and uses a fixed lhs. Keep the comparator and
expected true result consistent with existing positive cases, ensuring traversal
of QuantityCheck::rhs is covered.

---

Nitpick comments:
In `@crates/engine/src/parser/oracle_tests.rs`:
- Around line 23059-23062: Extract the duplicated has_unimpl helper into one
module-level function in oracle_tests.rs, then remove the three local
definitions and reuse the shared helper from each affected test. Preserve its
existing recursive check for Effect::Unimplemented and nested sub_ability
values.
- Around line 23278-23315: Strengthen
explicit_source_counter_gate_over_prior_target_stays_source_scoped with the same
reach guards as its sibling tests: assert parsed.parse_warnings is empty and
verify the relevant parsed effect is not Effect::Unimplemented before inspecting
its GenericEffect/HasCounters structure. Keep the existing semantic assertions
unchanged.
- Around line 23278-23281: Update the test comment above the affected test to
replace the incorrect CR 608.2c citation with the applicable
subject/anaphora-scope rule, such as CR 608.2k, while retaining CR 122.1 if
relevant. Use CR 608.2c only for comments describing resolution of written
instructions in order, not subject binding or keyword-list behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 08c6b562-6bd9-40b7-bce5-acf0de7c159a

📥 Commits

Reviewing files that changed from the base of the PR and between ae167b1 and cf2e109.

📒 Files selected for processing (6)
  • crates/engine/src/parser/oracle_effect/mod.rs
  • crates/engine/src/parser/oracle_effect/tests.rs
  • crates/engine/src/parser/oracle_nom/condition.rs
  • crates/engine/src/parser/oracle_tests.rs
  • crates/engine/tests/integration/gemstone_mine_depletion_sacrifice_6507.rs
  • crates/engine/tests/integration/main.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/engine/tests/integration/gemstone_mine_depletion_sacrifice_6507.rs
  • crates/engine/src/parser/oracle_nom/condition.rs

Comment thread crates/engine/src/parser/oracle_effect/tests.rs Outdated
Comment thread crates/engine/src/parser/oracle_effect/tests.rs
matthewevans pushed a commit to jeffrey701/phase that referenced this pull request Aug 1, 2026
… chosen target (phase-rs#6559 review)

The phase-rs#6507 predicate that binds a source-counter-gated rider pronoun to
SelfRef was one degree too broad: it also fired on Revelation of Power
("Target creature gets +2/+2 until end of turn. If it has a counter on
it, it also gains flying and lifelink"), whose intervening-if mis-scopes
the bare "it" to CountersOn{Source}. Binding that grant to the source
dropped flying/lifelink onto the one-shot Instant — the card lost its
second sentence (engine_regress), and CR 608.2k does not reach it (the
source is named by neither a cost nor a trigger condition).

Narrow the binding: only rebind the pronoun to the source when NO earlier
clause in the chain chose a typed target. Compute one gate at the
chunk-subject site and reuse it at both consumers (the chunk-subject
binding and the replace_target_with_parent guard):

    let binds_source_counter_pronoun = condition
        .is_some_and(condition_refs_source_object)
        && !chain_has_prior_typed_referent(builder.clauses(), false);

chain_has_prior_typed_referent is true for Revelation of Power (its prior
"Target creature gets +2/+2" is a Pump over a typed target) and false for
every depletion-land / counter rider (whose prior clause is "Add mana" or
"put a counter on ~", never a chosen target), so all 21 intended heals
keep SelfRef while Revelation of Power's grant returns to ParentTarget.
Chosen deliberately over chain_prior_referent_is_chosen_target, whose
has_typed_target_widened early-out returns false for a pump-of-a-target.

Adds source_counter_gate_over_prior_target_keeps_parent_not_self_ref
(pins Revelation of Power's grant to ParentTarget) and extends the
predicate unit test to drive the Sum/Difference two-operand walker arms.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@matthewevans
matthewevans force-pushed the fix/6507-depletion-sacrifice-rider branch from 7983ff2 to 4e4571f Compare August 1, 2026 18:03

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/engine/src/parser/oracle_static/shared.rs`:
- Around line 3262-3268: Add a verified CR annotation beside the
QuantityRef::CountersOn source-to-recipient rebinding in the parser, citing CR
608.2k and CR 611.3a. Explain that conditions on an attached object rebind
Source to Recipient while explicit source references remain unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8fa79c0e-5d99-4682-8dba-c49a48833667

📥 Commits

Reviewing files that changed from the base of the PR and between cf2e109 and 7983ff2.

📒 Files selected for processing (5)
  • crates/engine/src/parser/oracle_effect/mod.rs
  • crates/engine/src/parser/oracle_static/mod.rs
  • crates/engine/src/parser/oracle_static/shared.rs
  • crates/engine/src/parser/oracle_static/tests.rs
  • crates/engine/src/parser/oracle_tests.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/engine/src/parser/oracle_effect/mod.rs
  • crates/engine/src/parser/oracle_tests.rs

Comment thread crates/engine/src/parser/oracle_static/shared.rs Outdated
@matthewevans

Copy link
Copy Markdown
Member

Current-head maintainer hold for 4e4571f48c854aeb04e2e502ea135b7ea32204c7: this head adds the requested Revelation of Power cast-pipeline regression and narrows the prior typed-referent/source-counter binding repair. It is a materially new parser surface, so the existing parse-diff (updated 2026-08-01T17:54:47Z) and approvals/reviews on earlier commits are not current evidence. Required CI and a current full card-level parse artifact must complete before a final re-review or queue action.

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maintainer completion verified on e841edb: source-counter rebinding is constrained to the bare-pronoun/prior-target grammar, uses the shared recursive static-condition rebinder, and includes focused source/recipient plus RHS traversal coverage. Stale pre-rebase threads were resolved; current-head CI has been requested and merge-queue checks remain authoritative.

@matthewevans
matthewevans enabled auto-merge August 1, 2026 18:09
matthewevans pushed a commit to jeffrey701/phase that referenced this pull request Aug 1, 2026
… chosen target (phase-rs#6559 review)

The phase-rs#6507 predicate that binds a source-counter-gated rider pronoun to
SelfRef was one degree too broad: it also fired on Revelation of Power
("Target creature gets +2/+2 until end of turn. If it has a counter on
it, it also gains flying and lifelink"), whose intervening-if mis-scopes
the bare "it" to CountersOn{Source}. Binding that grant to the source
dropped flying/lifelink onto the one-shot Instant — the card lost its
second sentence (engine_regress), and CR 608.2k does not reach it (the
source is named by neither a cost nor a trigger condition).

Narrow the binding: only rebind the pronoun to the source when NO earlier
clause in the chain chose a typed target. Compute one gate at the
chunk-subject site and reuse it at both consumers (the chunk-subject
binding and the replace_target_with_parent guard):

    let binds_source_counter_pronoun = condition
        .is_some_and(condition_refs_source_object)
        && !chain_has_prior_typed_referent(builder.clauses(), false);

chain_has_prior_typed_referent is true for Revelation of Power (its prior
"Target creature gets +2/+2" is a Pump over a typed target) and false for
every depletion-land / counter rider (whose prior clause is "Add mana" or
"put a counter on ~", never a chosen target), so all 21 intended heals
keep SelfRef while Revelation of Power's grant returns to ParentTarget.
Chosen deliberately over chain_prior_referent_is_chosen_target, whose
has_typed_target_widened early-out returns false for a pump-of-a-target.

Adds source_counter_gate_over_prior_target_keeps_parent_not_self_ref
(pins Revelation of Power's grant to ParentTarget) and extends the
predicate unit test to drive the Sum/Difference two-operand walker arms.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@matthewevans
matthewevans force-pushed the fix/6507-depletion-sacrifice-rider branch from ffc15ba to 90b2aea Compare August 1, 2026 18:26
@matthewevans

Copy link
Copy Markdown
Member

Current-head maintainer hold for ffc15ba3a746c94b0f6aa28e9d680c234f0fd0cc: this is a clean linear rebase onto current main. The direct delta from the reviewed 6b8e9c55 head contains only upstream main files; none of this PR’s nine parser/test files changed. The constrained source-counter rebinding, the source/recipient/prior-target/RHS-counter coverage, and Gemstone Mine/Revelation cast-pipeline regressions remain the approved implementation.

The rebase started a fresh CI run. Rust lint/parser gate and WASM are in progress; Rust-test shards, card-data/parse-diff, Tauri, frontend, and Lobby checks are pending/queued. The visible 17-card parse artifact predates this head, so it is not current-head evidence yet. Auto-merge is disabled until current artifacts complete successfully. No merge-queue action during this hold.

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approving the unchanged, reviewed maintainer completion after rebase onto the current merge-queue base.

@matthewevans
matthewevans enabled auto-merge August 1, 2026 18:27
@matthewevans matthewevans removed their assignment Aug 1, 2026
@matthewevans
matthewevans disabled auto-merge August 1, 2026 18:27
@matthewevans

Copy link
Copy Markdown
Member

Correction: the current head is 90b2aea7e347db6f8ae41d1a06ddaffa8eea889d. It is a clean rebase of the already-approved #6559 implementation onto current main (616ee41e5c): its PR diff still contains exactly the nine reviewed source-counter parser/test files (947 additions, 6 deletions). The two-file direct delta from the prior rebased head is upstream main content, not a new PR change.

Current CI has restarted and is not yet merge evidence; auto-merge is disabled. No merge-queue action until current-head required checks and the current parse artifact complete successfully.

jeffrey701 and others added 8 commits August 1, 2026 11:41
…Gemstone Mine phase-rs#6507)

The depletion-land sacrifice rider ("If there are no mining counters on
this land, sacrifice it.") parsed to Sacrifice{ParentTarget}. A mana
ability has no targets (CR 605.1a), so ParentTarget resolved to an empty
set and the sacrifice silently no-op'd — the land never left play.

Root cause is a parse-time anaphor mis-binding, not a runtime gap: the
chunk-subject threading in the effect-chain parser already binds a bare
"it" to SelfRef when the gating condition references the source object,
via condition_refs_source_object. That predicate recognized the
source-tapped / source-entered / source-attached conditions but not a
source-scoped counter threshold (QuantityCheck over CountersOn{Source}),
so the counter-gated riders fell through to ParentTarget (and, on typed
triggers, to TriggeringSource).

Extend condition_refs_source_object with a QuantityCheck arm that returns
true when either side reads counters on the source, via a new exhaustive
QuantityExpr walker (no wildcard — a future variant must be classified).
This single predicate extension drives both existing consumers: the
chunk-subject threading now supplies SelfRef, and the ParentTarget
rewrite guard now skips these chunks. Bindings become source-correct for
the Mercadian depletion lands (Peat Bog, Hickory Woodlot, Remote Farm,
Sandstone Needle, Saprazzan Skerry), Gemstone Mine, Tourach's Gate,
Daredevil Dragster, Last Light of Durin's Day, ED-E, and the whole
source-counter-conditioned rider class (~21 cards).

No runtime files change. CR 122.1 + CR 608.2k annotate the new arm.

Closes phase-rs#6507

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… chosen target (phase-rs#6559 review)

The phase-rs#6507 predicate that binds a source-counter-gated rider pronoun to
SelfRef was one degree too broad: it also fired on Revelation of Power
("Target creature gets +2/+2 until end of turn. If it has a counter on
it, it also gains flying and lifelink"), whose intervening-if mis-scopes
the bare "it" to CountersOn{Source}. Binding that grant to the source
dropped flying/lifelink onto the one-shot Instant — the card lost its
second sentence (engine_regress), and CR 608.2k does not reach it (the
source is named by neither a cost nor a trigger condition).

Narrow the binding: only rebind the pronoun to the source when NO earlier
clause in the chain chose a typed target. Compute one gate at the
chunk-subject site and reuse it at both consumers (the chunk-subject
binding and the replace_target_with_parent guard):

    let binds_source_counter_pronoun = condition
        .is_some_and(condition_refs_source_object)
        && !chain_has_prior_typed_referent(builder.clauses(), false);

chain_has_prior_typed_referent is true for Revelation of Power (its prior
"Target creature gets +2/+2" is a Pump over a typed target) and false for
every depletion-land / counter rider (whose prior clause is "Add mana" or
"put a counter on ~", never a chosen target), so all 21 intended heals
keep SelfRef while Revelation of Power's grant returns to ParentTarget.
Chosen deliberately over chain_prior_referent_is_chosen_target, whose
has_typed_target_widened early-out returns false for a pump-of-a-target.

Adds source_counter_gate_over_prior_target_keeps_parent_not_self_ref
(pins Revelation of Power's grant to ParentTarget) and extends the
predicate unit test to drive the Sum/Difference two-operand walker arms.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@matthewevans
matthewevans force-pushed the fix/6507-depletion-sacrifice-rider branch from 90b2aea to abcbf9d Compare August 1, 2026 18:41
@matthewevans matthewevans self-assigned this Aug 1, 2026

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approving the unchanged maintainer completion after the final rebase onto current main.

@matthewevans
matthewevans enabled auto-merge August 1, 2026 18:42
@matthewevans matthewevans removed their assignment Aug 1, 2026
@matthewevans matthewevans self-assigned this Aug 1, 2026
@matthewevans
matthewevans disabled auto-merge August 1, 2026 19:04
@matthewevans

Copy link
Copy Markdown
Member

Current-head hold for b79066e8af6cb582fd0d5fa8076da28fdd4d6ff2: the new functional delta moves the constrained leading bare-recipient counter-condition rebind to the ClauseIr condition before lowering, replacing the post-lowering GenericEffect-only rewrite. Hand review confirms the same narrow grammar gate, prior-typed-target guard, source/recipient/RHS recursive traversal, and Revelation-of-Power protection; the final commit only clarifies that ownership in code comments.

Current CI is still running (Rust lint/parser gate, both Rust shards, card data/coverage, and frontend); WASM, Tauri, Lobby, security, contributor trust, and CodeRabbit are successful. The 17-card/5-signature parse artifact predates this head, so it is not current-head evidence yet. Auto-merge is disabled. Do not queue until current CI and parse artifacts complete successfully.

@matthewevans matthewevans removed their assignment Aug 1, 2026
@matthewevans
matthewevans added this pull request to the merge queue Aug 1, 2026
Merged via the queue into phase-rs:main with commit f1e01d9 Aug 1, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Bug fix needs-maintainer AI-contribution PR requires human triage (Non-dev track or unresolved gaps)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants